一覧に戻る

AWS LambdaでSSRするフロントエンドをデプロイする

#Node.js#AWS#lambda#Svelte#SvelteKit

TL;DR

  • QGIS LABではLambda Web Adapter(LWA)を利用して、Node.jsコンテナ上でSvelteKitを動かしてサーバーサイドレンダリング(SSR)している
  • LWAはストリーミングをサポートしているので、SvelteKitのStreamingを問題なく利用できる
  • CloudFrontの共有キャッシュでstale-while-revalidateを利用してコールドスタートの影響を軽減
  • このような工夫をすれば、LWAでSSRするアプリケーションをデプロイすることは実用的

QGIS LAB

https://qgis.mierune.co.jp/

筆者所属のMIERUNEが運営する、QGISの情報サイトです。2024年10月にリリースしたこのWebサイトは、AWS Lambda上で動作しています。フロントエンドフレームワークにはSvelteKitを採用しており、ヘッドレスCMSと通信したうえで、SSRによりHTMLをレスポンスしています。

Lambdaでコンテナを動かすLambda Web Adapter

AWS Lambdaはコンテナイメージを動かすことも出来ます。ただしコンテナであっても通常のLambdaと同様、handler関数を実装する必要があります。これではLambda専用コンテナとなってしまいますが、ポータビリティを売りとするのがコンテナなので、外界とは普通のrequest/responseでコミュニケーションしたいところです。Lambda Web Adapterはそれを可能とします。

https://aws.amazon.com/jp/builders-flash/202402/lambda-container-runtime/

Lambda Wed Adapterを利用することで、任意のHTTPをしゃべるコンテナイメージをLambda上で動作させることができます。また、Lambdaは「関数URL(Function URLs)」でHTTPSリクエストを受け付けるエンドポイントを取得することが出来るため、最小構成ではLambda単独でWebサーバーをデプロイすることが出来ます。

SvelteKitをNode.jsのコンテナイメージ上で動作させる

カレントディレクトリにpackage.jsonbuildディレクトリ(SvelteKitのビルドアーティファクト)があるとして、下記のようなコンテナイメージ書くだけでよいです。

FROM public.ecr.aws/docker/library/node:22-bookworm-slim

# Lambda WebAdapter
COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:0.8.4 /lambda-adapter /opt/extensions/lambda-adapter
ENV PORT=3000
ENV READINESS_CHECK_PATH=/
ENV AWS_LWA_INVOKE_MODE=response_stream

WORKDIR /app
COPY package.json package-lock.json ./
# ランタイムに必要な依存関係のみをインストール
RUN npm install --omit=dev 

COPY ./build ./build

CMD ["node", "/app/build/index.js"]

このイメージをECRにデプロイし、Lambdaをこのイメージを参照してデプロイすればOKです。気になるのはコールドスタートですが、 体感で2,3秒程度です(推測するな測定せよという声が聞こえてくる)。関数が実行されるにつれコンテナイメージのキャッシュが暖まってくるらしく(出典忘れた)、暖まった状態でこの体感速度でした。コンテナイメージが大きく変更されない限りは、イメージのバージョンが変わってもキャッシュは再利用されていそうです。

また、DockerfileのLWAのイディオムのうち:

ENV AWS_LWA_INVOKE_MODE=response_stream

この記述は、ストリーミングを有効にするために必要です。これによりSvelteKitのStreamingがLWAでも動作します。

参考:Lambdaの過去4週間のメトリクス

maximumがコールドスタート時と理解できます

ブレはあるものの同時実行数の平均値は5程度に収束しています。同時実行数の最大を1000としたとき、今の100倍アクセスがあっても大丈夫そう、しらんけど。

:::note info このあたりのメトリクスはもちろんアクセス数の多い・少ないの影響は受けるので、目安としてご理解ください。QGIS LABは現状、膨大なリクエストが発生するようなWebサイトではありません。発生するようになったら嬉しいですね。 :::

CloudFrontでキャッシュを頑張る

Lambdaをそのまま晒すことはないので、以下のような構成になると思います。

graph LR
c((client)) --ビューアーリクエスト--> CloudFront --オリジンリクエスト--> Lambda/SvelteKit

なおCloudFront-Lambda間でOACを利用することでLambdaへの直接アクセスを防げます

https://qiita.com/Kanahiro/items/85573c9ae724df435a6a

SvelteKitは、load()関数内で以下のようにレスポンスヘッダを設定出来ます:

setHeaders({
    'Cache-Control': 'max-age=0, s-maxage=30, stale-while-revalidate=86400, stale-if-error=86400'
});

CloudFrontはstale-while-revalidateディレクティブを解釈してくれるので、キャッシュが完全に期限切れならオリジンリクエスト、SWRのTTL内ならキャッシュをレスポンス・キャッシュ更新、キャッシュTTL内ならキャッシュをレスポンス、という処理を、clientとCloudFrontの間で実施してくれます。

https://aws.amazon.com/jp/about-aws/whats-new/2023/05/amazon-cloudfront-stale-while-revalidate-stale-if-error-cache-control-directives/

このようにSWRを利用してコンテンツの鮮度を保ちつつ共有キャッシュを活用しています。Lambdaの弱点であるコールドスタートの影響も一定程度緩和できていると思います。

参考:キャッシュヒット率

足して100%にならないのでその分がSWRのRefresh Hitだと勝手に思っている(情報求)。俺たちは雰囲気でCDNをやっている。

結論:LWAでSSRは可能!

  • QGIS LABのリリース後、現状までの1ヶ月強の期間で特に不都合なく、パフォーマンスもよく配信出来ているので、SSRしたいWebサイトのデプロイ方法としてAWS Lambda + Lambda Web Adapterは十分に実用的であると言えます。Lambdaは非常に安いのでインフラコストも低く抑えられています。
  • また弊社はSvelteKitを標準として採用していますが、コンテナイメージでNode.jsが動いているのでNext.jsでも同じことが出来るはずです。
  • 静的アセットもLambda関数を通じて配信される点については留意が必要です(関数の実行数が消費されることもあり)。たとえばsveltekit-adapter-awsは静的アセットはS3から配信するという工夫をしていたりします。複雑性は結構高まりそうですが、改善の余地はありそうです。

:::note info 以下にもQGIS LABに関する技術情報が記載されています :::

https://zenn.dev/mierune/articles/bb3e72b72892e4